<!--
 * @Author: hongbin
 * @Date: 2025-09-04 19:49:10
 * @LastEditors: hongbin
 * @LastEditTime: 2025-09-04 19:51:45
 * @Description: 
-->
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>噪声球粒子切换</title>
        <style>
            html,
            body {
                height: 400vh;
                margin: 0;
                background: #000;
                color: #fff;
                overflow-x: hidden;
            }
            canvas {
                display: block;
                position: fixed;
                top: 0;
                left: 0;
                z-index: 1;
            }
        </style>
    </head>
    <body>
        <div style="height: 100vh"></div>
        <canvas id="c"></canvas>
        <div style="padding-left: 10px">
            <hr style="border-color: #444" />
            <h1 style="text-align: center">粒子切换</h1>

            <div style="display: flex; align-items: flex-start; flex-direction: row">
                <img
                    style="width: 650px; height: 400px"
                    src="./image copy.png"
                    alt="model" />
                <div>
                    <h3 style="text-align: center; width: 650px">延迟移动</h3>
                    <pre>
    根据目标位置的 Y 轴高度 计算速度
    float ny = (position.y - minY) / (maxY - minY) ;

    y 越小 → 切换越快
    y 越低越大
    float base = 2.0 - ny; 
    base = pow(base, 1.);
    y 越低越快 产生一些负数被归0后不产生移动
    float speedFactor = base - (1.0 - iProgress) * 2.0;

    组合进度（加上 y 的速度差）
    float localProgress = smoothstep(0.0, 1.0, (iProgress ) * (speedFactor));

                </pre
                    >
                </div>
            </div>
            <div style="display: flex; align-items: flex-start; flex-direction: row">
                <img
                    style="width: 650px; height: 400px"
                    src="./image copy 3.png"
                    alt="model" />
                <div>
                    <h3 style="text-align: center; width: 650px">背后剔除</h3>
                    <pre>
    粒子指向外的方向（假设从球心发散）
    vec3 normalDir = normalize(position);

    相机方向
    vec3 camDir = normalize(cameraPosition - position);

    dot > 0 表示朝向相机，dot < 0 表示背对相机
    vVisible = dot(normalDir, camDir);
                </pre
                    >
                </div>
            </div>

            <div style="display: flex; align-items: flex-start; flex-direction: column">
                <h3 style="text-align: center; width: 650px">模型</h3>
                <img
                    style="width: 650px; height: 400px"
                    src="./image.png"
                    alt="model" />
            </div>
        </div>
        <!-- Import Map (ESM) -->
        <script type="importmap">
            {
                "imports": {
                    "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
                    "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/",
                    "lil-gui": "https://unpkg.com/lil-gui@0.19/dist/lil-gui.esm.min.js"
                }
            }
        </script>

        <script type="module">
            import * as THREE from "three";
            import { OrbitControls } from "three/addons/controls/OrbitControls.js";
            import GUI from "lil-gui";
            import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
            import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";

            const canvas = document.getElementById("c");
            const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
            renderer.setSize(innerWidth, innerHeight);
            renderer.setPixelRatio(Math.min(devicePixelRatio, 2));

            const scene = new THREE.Scene();
            scene.background = new THREE.Color(0x000000);

            // scene.fog = new THREE.FogExp2(0xcccccc, 1);
            scene.fog = new THREE.FogExp2(0x333333, 0.4);

            const startEye = {
                cameraPosition: new THREE.Vector3(0, 0.2, 3.5),
                controlsTarget: new THREE.Vector3(0, 0.2, 0),
            };

            const camera = new THREE.PerspectiveCamera(55, innerWidth / innerHeight, 0.1, 1000);
            // camera.position.set(0, 0, 3);
            camera.position.copy(startEye.cameraPosition);
            scene.add(camera);

            const controls = new OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true;
            controls.enabled = false;
            controls.target.copy(startEye.controlsTarget);

            function sampleFromSphere(geometric, particleCount, mode, useUnique = true, overflow = false) {
                // 都约束到数组中
                if (Array.isArray(geometric)) {
                } else {
                    geometric = [geometric];
                }

                const pos = [];
                const cols = [];
                let vertexCount = 0;
                let spherePositions = [];

                geometric.forEach(item => {
                    const item_pos = item.attributes.position.array;
                    pos.push(...item_pos);

                    const item_col = item.attributes?.color?.array;
                    item_col && cols.push(...item_col);
                });

                if (useUnique) {
                    const set = new Set();

                    for (let i = 0; i < pos.length; i += 3) {
                        const x = pos[i].toFixed(5);
                        const y = pos[i + 1].toFixed(5);
                        const z = pos[i + 2].toFixed(5);
                        const key = `${x},${y},${z}`;
                        if (!set.has(key)) {
                            set.add(key);
                            spherePositions.push(pos[i], pos[i + 1], pos[i + 2]);
                        }
                    }
                } else {
                    spherePositions = pos;
                }

                vertexCount = spherePositions.length / 3;

                // const spherePositions = sphereGeo.attributes.position.array;
                // const vertexCount = spherePositions.length / 3;

                return randomUniqueSampling(spherePositions, particleCount, cols);

                const positions = new Float32Array(particleCount * 3);

                for (let i = 0; i < particleCount; i++) {
                    if (mode === "subsample") {
                        // --- 稀疏采样 ---
                        const idx = Math.floor(Math.random() * vertexCount);
                        positions[i * 3 + 0] = spherePositions[idx * 3 + 0];
                        positions[i * 3 + 1] = spherePositions[idx * 3 + 1];
                        positions[i * 3 + 2] = spherePositions[idx * 3 + 2];
                    } else if (mode === "interpolated") {
                        // --- 插值采样 ---
                        const idx1 = Math.floor(Math.random() * vertexCount);
                        const idx2 = Math.floor(Math.random() * vertexCount);
                        const t = Math.random(); // 插值权重

                        const v1x = spherePositions[idx1 * 3 + 0];
                        const v1y = spherePositions[idx1 * 3 + 1];
                        const v1z = spherePositions[idx1 * 3 + 2];

                        const v2x = spherePositions[idx2 * 3 + 0];
                        const v2y = spherePositions[idx2 * 3 + 1];
                        const v2z = spherePositions[idx2 * 3 + 2];

                        positions[i * 3 + 0] = THREE.MathUtils.lerp(v1x, v2x, t);
                        positions[i * 3 + 1] = THREE.MathUtils.lerp(v1y, v2y, t);
                        positions[i * 3 + 2] = THREE.MathUtils.lerp(v1z, v2z, t);
                    } else {
                        const idx = i % vertexCount;

                        // 超出点数据 直接指向一个足够大的值
                        if (i >= vertexCount && overflow) {
                            positions[i * 3 + 0] = spherePositions[idx * 3 + 0] * 100;
                            positions[i * 3 + 1] = spherePositions[idx * 3 + 1];
                            positions[i * 3 + 2] = spherePositions[idx * 3 + 2] - 100;
                        } else {
                            // --- 按照顺序采样 ---
                            positions[i * 3 + 0] = spherePositions[idx * 3 + 0];
                            positions[i * 3 + 1] = spherePositions[idx * 3 + 1];
                            positions[i * 3 + 2] = spherePositions[idx * 3 + 2];
                        }

                        const offset = Math.random() * 0.1;

                        // 带偏移
                        // positions[i * 3 + 0] =  offset +  ( (-offset + spherePositions[idx * 3 + 0]) * (Math.random() * 0.05 + 0.95));
                        // positions[i * 3 + 1] =  offset +  ( (-offset + spherePositions[idx * 3 + 1]) * (Math.random() * 0.05 + 0.95));
                        // positions[i * 3 + 2] =  offset +  ( (-offset + spherePositions[idx * 3 + 2]) * (Math.random() * 0.05 + 0.95));
                    }
                }

                return positions;
            }

            const dracoLoader = new DRACOLoader();
            dracoLoader.setDecoderPath("jsm/libs/draco/gltf/");

            const loader = new GLTFLoader();
            loader.setDRACOLoader(dracoLoader);

            loader.load("sphere.glb", function (gltf) {
                console.log("sphere", gltf);

                // scene.add(gltf.scene);

                const 文本 = gltf.scene.getObjectByName("文本");

                文本.material = whiteText();

                scene.add(文本);

                {
                    const 文本 = gltf.scene.getObjectByName("文本001");

                    文本.material = whiteText(true, new THREE.Color(0xbbbbbb) );

                    scene.add(文本);
                }
                {
                    const 文本 = gltf.scene.getObjectByName("文本002");

                    文本.material = whiteText(true);

                    scene.add(文本);
                }

                const part1 = gltf.scene.getObjectByName("1");
                const part2 = gltf.scene.getObjectByName("2");
                const part3 = gltf.scene.getObjectByName("3");
                const part4 = gltf.scene.getObjectByName("4");

                const uniquePosition1 = getUniqueData(part1.geometry);
                const color1 = getColorAttribute(uniquePosition1, new THREE.Color("#D5D5D7"));
                const uniquePosition2 = getUniqueData(part2.geometry);
                const color2 = getColorAttribute(uniquePosition2, new THREE.Color("#33375C"));
                const uniquePosition3 = getUniqueData(part3.geometry);
                const color3 = getColorAttribute(uniquePosition3, new THREE.Color("#4D868B"));

                const uniquePosition4 = getUniqueData(part4.geometry);

                // const count = uniquePosition3.length / 3 + uniquePosition2.length / 3 + uniquePosition1.length / 3;
                const count = uniquePosition4.length / 3;

                const positions = sampleFromSphere(
                    [
                        { attributes: { position: { array: uniquePosition1 }, color: { array: color1 } } },
                        { attributes: { position: { array: uniquePosition2 }, color: { array: color2 } } },
                        { attributes: { position: { array: uniquePosition3 }, color: { array: color3 } } },
                    ],
                    count,
                    undefined,
                    false,
                    true
                );

                const geo = new THREE.BufferGeometry();
                geo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
                positions.colors && geo.setAttribute("sphereColor", new THREE.BufferAttribute(positions.colors, 3));
                positions.overs && geo.setAttribute("over", new THREE.BufferAttribute(positions.overs, 1));

                const points = new THREE.Points(geo, material);
                scene.add(points);

                {
                    const positions = sampleFromSphere({ attributes: { position: { array: uniquePosition4 } } }, count);

                    geo.setAttribute("planePosition", new THREE.BufferAttribute(positions, 3));
                }

                addRandomAttribute(geo);

                addColorAttribute(geo, [
                    { pos: 0.0, color: [1, 1, 1] }, // 白
                    { pos: 0.6, color: [0.03, 0.03, 1] }, // 蓝 (20%)
                    { pos: 1.0, color: [0.07, 1.238, 1.258] }, // 青 (40%)
                ]);
            });

            // const sphereGeo = new THREE.SphereGeometry(1.0, 128, 128);
            // const sphereGeo = new THREE.IcosahedronGeometry(1.0, 32);
            //！去除重复顶点
            // const uniquePosition = getUniqueData(sphereGeo);
            // const uniqueLength = uniquePosition.length;

            // console.log("原顶点数量：", sphereGeo.attributes.position.count, "去重后顶点数量：", uniqueLength / 3);

            // const count = sphereGeo.attributes.position.count;
            // const count = uniqueLength / 3;
            // const count = 9;

            // const positions = sampleFromSphere(sphereGeo, count);
            // const positions = sampleFromSphere({ attributes: { position: { array: uniquePosition } } }, count);

            /** 柏林噪声片段 */
            const perlinNoiseFragment = `
                        //	Classic Perlin 3D Noise
                        //	by Stefan Gustavson
                        //
                        vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
                        vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
                        vec3 fade(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}

                        float noise(vec3 P){
                          vec3 Pi0 = floor(P); // Integer part for indexing
                          vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1
                          Pi0 = mod(Pi0, 289.0);
                          Pi1 = mod(Pi1, 289.0);
                          vec3 Pf0 = fract(P); // Fractional part for interpolation
                          vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0
                          vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
                          vec4 iy = vec4(Pi0.yy, Pi1.yy);
                          vec4 iz0 = Pi0.zzzz;
                          vec4 iz1 = Pi1.zzzz;

                          vec4 ixy = permute(permute(ix) + iy);
                          vec4 ixy0 = permute(ixy + iz0);
                          vec4 ixy1 = permute(ixy + iz1);

                          vec4 gx0 = ixy0 / 7.0;
                          vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5;
                          gx0 = fract(gx0);
                          vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0);
                          vec4 sz0 = step(gz0, vec4(0.0));
                          gx0 -= sz0 * (step(0.0, gx0) - 0.5);
                          gy0 -= sz0 * (step(0.0, gy0) - 0.5);

                          vec4 gx1 = ixy1 / 7.0;
                          vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5;
                          gx1 = fract(gx1);
                          vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1);
                          vec4 sz1 = step(gz1, vec4(0.0));
                          gx1 -= sz1 * (step(0.0, gx1) - 0.5);
                          gy1 -= sz1 * (step(0.0, gy1) - 0.5);

                          vec3 g000 = vec3(gx0.x,gy0.x,gz0.x);
                          vec3 g100 = vec3(gx0.y,gy0.y,gz0.y);
                          vec3 g010 = vec3(gx0.z,gy0.z,gz0.z);
                          vec3 g110 = vec3(gx0.w,gy0.w,gz0.w);
                          vec3 g001 = vec3(gx1.x,gy1.x,gz1.x);
                          vec3 g101 = vec3(gx1.y,gy1.y,gz1.y);
                          vec3 g011 = vec3(gx1.z,gy1.z,gz1.z);
                          vec3 g111 = vec3(gx1.w,gy1.w,gz1.w);

                          vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110)));
                          g000 *= norm0.x;
                          g010 *= norm0.y;
                          g100 *= norm0.z;
                          g110 *= norm0.w;
                          vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111)));
                          g001 *= norm1.x;
                          g011 *= norm1.y;
                          g101 *= norm1.z;
                          g111 *= norm1.w;

                          float n000 = dot(g000, Pf0);
                          float n100 = dot(g100, vec3(Pf1.x, Pf0.yz));
                          float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z));
                          float n110 = dot(g110, vec3(Pf1.xy, Pf0.z));
                          float n001 = dot(g001, vec3(Pf0.xy, Pf1.z));
                          float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z));
                          float n011 = dot(g011, vec3(Pf0.x, Pf1.yz));
                          float n111 = dot(g111, Pf1);

                          vec3 fade_xyz = fade(Pf0);
                          vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z);
                          vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
                          float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
                          return 2.2 * n_xyz;
                        }
                        `;

            // --------- 顶点着色器 ----------
            const vertexShader = /* glsl */ `
                          // ---- Ashima Noise ----
                            ${perlinNoiseFragment}
                          uniform float uTime;
                          uniform float uAmp;
                          uniform float uFreq;
                          uniform float uSpeed;
                          uniform float uSize;
                          varying float v_noise;
                          varying vec3 vPos;
                          varying float vVisible;
                          varying float vFogDepth;
                          attribute vec3 planePosition;
                          attribute vec3 vColor;
                          attribute vec3 sphereColor;
                          attribute float over;
                          varying float vOver;
                          varying vec3 vSphereColor;
                          varying vec3 vvColor;
                          varying float vRandom;
                          varying float vNy;
                          uniform float iProgress;

                          void main() {
                            vvColor = vColor;
                            vOver = over;
                            vSphereColor = sphereColor;

                            float minY = -1.0;
                            float maxY = 1.0;

                            // 归一化 Y 到 [0,1]
                            float ny = (position.y - minY) / (maxY - minY) ;
                            vNy = ny;

                            // y 越小 → 切换越快
                            float base = 2.0 - ny; // y 越低越大
                            base = pow(base, 1.);
                            float speedFactor = base - (1.0 - iProgress) * 2.0; // y 越低越快

                            // 进度（加上 y 的速度差）
                            float localProgress = smoothstep(0.0, 1.0, (iProgress ) * (speedFactor));

                            vNy = 1. - localProgress;

                            vec3 dyPosition = position;

                            dyPosition.y += (1.0 - iProgress) * 2.;

                            vec3 dyPlanePosition = planePosition;

                            // dyPlanePosition.y += noise(dyPlanePosition / 20. + vec3(0.0,0.0,uTime*uSpeed)) / 4.;

                            vec3 newPos = mix(dyPlanePosition, dyPosition, localProgress);

                            // vec3 dir = mix(normalize(newPos),newPos,iProgress);
                            // vec3 dir = normalize(newPos);
                            vec3 dir = newPos;

                            // uFreq 系数 两个形态过渡时系数变动
                            // 慢速的粒子应该同样缓慢变换
                            float fs = clamp(((1.0 - iProgress) * ( 2. + base )) , 1., 2. );

                            float n = noise(dir * ( uFreq / fs ) + vec3(0.0,0.0,uTime*uSpeed));

                            // float disp = n * max(0.03,uAmp * clamp(iProgress,0.0,1.0) );
                            float disp = n * uAmp;

                            // if(iProgress > ny){
                            //     disp = n *uAmp;
                            // }

                            vec3 displaced = newPos + disp * dir;

                            v_noise = disp;

                            // displaced.y -= n * 0.1;

                            vPos = displaced;
                            // vPos = newPos;

                            vec4 mvPosition = modelViewMatrix * vec4( displaced, 1.0 );

                            gl_Position = projectionMatrix * mvPosition;
                            // gl_PointSize = uSize;
                            gl_PointSize = uSize * (3. / - mvPosition.z );

                            // 粒子指向外的方向（假设从球心发散）
                            // vec3 normalDir = mix(displaced.xyz, normalize(displaced.xyz) , iProgress);
                            vec3 normalDir = normalize(displaced.xyz);

                            // 相机方向
                            vec3 camDir = normalize(cameraPosition - displaced.xyz);

                            // dot > 0 表示朝向相机，dot < 0 表示背对相机
                            // vVisible = dot(normalDir, camDir) * smoothstep(0.,1.,clamp(iProgress/vNy,0.0,1.0));
                            vVisible = dot(normalDir, camDir) + ((1.0 + vNy) - iProgress);

                            // if(abs(length(displaced) - length(position)) > 0.1){
                            //     vVisible = 0.;
                            // }

                            vFogDepth = - mvPosition.z;
                          }
                        `;

            // --------- 片元着色器 ----------
            const fragmentShader = /* glsl */ `
                          precision highp float;
                          varying float v_noise;
                          varying vec3 vPos;
                          varying float vVisible;
                          varying vec3 vvColor;
                          varying float vFogDepth;
                          varying float vNy;
                          varying vec3 vSphereColor;
                          varying float vOver;
                          uniform float uTime;
                          uniform float fogDensity;
                          uniform vec3 fogColor;
                          uniform float iProgress;
                          uniform vec3 lightPosition;
                          uniform vec3 uHighlightCenter;

                          void main() {
                            // 背后剔除 (背对相机 → 丢弃)
                            if(vVisible < -0.2) discard;

                            // 粒子圆形渐变
                            float dist = 1.0 - length(gl_PointCoord - 0.5);

                            dist -= vOver * iProgress;

                            float intensity = clamp(vVisible * dot(normalize(lightPosition),normalize(vec3(clamp(v_noise,0.,1.)))),0.,1.);
                            
                            // 正面中心
                            float cd = distance(vPos,vec3(0., (1.0 - iProgress) ,1. + (1.0 - iProgress) * -4.));
                            cd = pow(cd,3.);
                            cd = smoothstep(0.,0.3,cd);

                            // TODO：顶点着色器里计算位置影响更好
                            // 鼠标影响高亮 uHighlightCenter
                            float highlight_x = distance(vPos.x, uHighlightCenter.x);

                            highlight_x = 1.0 +  smoothstep(.4, 0., highlight_x) * abs(v_noise) * 50.;

                            // 跟随时间高亮
                            float t_highlight_x = distance(vPos.x, 3.5 * (fract(uTime / 10.) - 0.5) * 2.);
                            t_highlight_x = 1.0 +  smoothstep(.4, 0., t_highlight_x) * abs(v_noise) * 100.;

                            highlight_x += t_highlight_x;


                            // cd += highlight * 0.5;

                            // 颜色过渡 按照ny高度差来决定 越下面越快完成
                            float p = clamp( iProgress / (vNy+0.1) , 0. , 1. );
                            float h = (1.0 + intensity + 20. * abs(v_noise));
                            
                            gl_FragColor = vec4(mix(vvColor * 3.5,vSphereColor * cd * h, p) * highlight_x, dist);
                            // gl_FragColor = vec4(vec3(highlight_x) , dist);
                        
                            if (dist < 0.5) discard;

                            #include <tonemapping_fragment>
                            #include <colorspace_fragment>
		                    
                            float fogFactor = 1.0 - exp( - fogDensity * fogDensity * vFogDepth * vFogDepth );
	                        gl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor * (1.0 - clamp(iProgress,0.0,1.0)) );
                            

                          }
                        `;

            // --------- 材质 + uniform ----------
            const uniforms = {
                uTime: { value: 0 },
                iProgress: { value: 0 },
                // uAmp: { value: 0.15 },
                uAmp: { value: 0.09 },
                uFreq: { value: 3.0 },
                // uFreq: { value: 1 },
                uSpeed: { value: 0.1 },
                uSize: { value: 4 },
                fogDensity: { value: scene.fog?.density || 0 },
                fogColor: { value: scene.fog?.color || new THREE.Color(0xffffff) },
                lightPosition: { value: camera.position },
                // 定义一个高亮区域
                uHighlightCenter: { value: new THREE.Vector3(1, 0, 0) },
            };

            const material = new THREE.ShaderMaterial({
                vertexShader,
                fragmentShader,
                uniforms,
                transparent: true,
                blending: THREE.AdditiveBlending,
                // side: THREE.DoubleSide,
                side: THREE.FrontSide,
                defines: {
                    USE_FOG: true,
                },
            });

            // const geo = new THREE.BufferGeometry();
            // geo.setAttribute("position", new THREE.BufferAttribute(positions, 3));

            // const points = new THREE.Points(geo, material);
            // scene.add(points);

            // --------- lil-gui ----------
            const gui = new GUI();
            gui.add(uniforms.uAmp, "value", 0, 1, 0.01).name("Amplitude");
            gui.add(uniforms.uFreq, "value", 0.1, 5, 0.1).name("Frequency");
            gui.add(uniforms.uSpeed, "value", 0, 2, 0.01).name("Speed");
            gui.add(uniforms.uSize, "value", 1, 10, 0.1).name("Point Size");
            gui.add(uniforms.uHighlightCenter.value, "x", -3, 3, 0.1).name("Highlight Center X");
            const p_gui = gui
                .add(uniforms.iProgress, "value", 0, 1, 0.01)
                .name("Progress")
                .onChange(e => {
                    camera.position.y = 0.2 * (1 - e);
                    controls.target.y = 0.2 * (1 - e);
                });
            gui.add(
                {
                    f: () => {
                        console.log(camera.position);
                        console.log(controls.target);
                    },
                },
                "f"
            ).name("视角");

            let handle = 0;
            const reset = () => {
                handle = requestAnimationFrame(reset);

                uniforms.iProgress.value += 0.01;
                p_gui.updateDisplay();

                if (uniforms.iProgress.value >= 1) {
                    cancelAnimationFrame(handle);
                }
            };

            gui.add(
                {
                    f: () => {
                        uniforms.iProgress.value = 0;

                        reset();
                    },
                },
                "f"
            ).name("play");

            const clock = new THREE.Clock();
            renderer.setAnimationLoop(() => {
                uniforms.uTime.value = clock.getElapsedTime();
                controls.update();
                renderer.render(scene, camera);
            });

            window.addEventListener("resize", () => {
                camera.aspect = innerWidth / innerHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(innerWidth, innerHeight);
            });

            // 鼠标移动监听
            window.addEventListener("mousemove", e => {
                uniforms.uHighlightCenter.value.x = 3 * ((e.clientX / innerWidth) * 2 - 1);
            });

            function getUniqueData(geometry) {
                const pos = geometry.attributes.position.array;
                let spherePositions = [];

                // 去重模式
                const set = new Set();
                for (let i = 0; i < pos.length; i += 3) {
                    const x = pos[i].toFixed(5);
                    const y = pos[i + 1].toFixed(5);
                    const z = pos[i + 2].toFixed(5);
                    const key = `${x},${y},${z}`;
                    if (!set.has(key)) {
                        set.add(key);
                        spherePositions.push(pos[i], pos[i + 1], pos[i + 2]);
                    }
                }

                return spherePositions;
            }

            function getColorAttribute(pos, three_color) {
                const color = [];

                for (let i = 0; i < pos.length; i += 3) {
                    color.push(three_color.r, three_color.g, three_color.b);
                }

                return color;
                // return new THREE.BufferAttribute(new Float32Array(color), 3);
            }

            function addRandomAttribute(geometry) {
                const pos = geometry.attributes.planePosition.array;
                const count = pos.length / 3;

                const randoms = new Float32Array(count);

                for (let i = 0; i < count; i++) {
                    randoms[i] = Math.random() * 0.8 + 0.2;
                    // randoms[i] = (Math.random() - 0.5) * 2;
                }
                geometry.setAttribute("vRandom", new THREE.BufferAttribute(randoms, 1));
                geometry.attributes.vRandom.needsUpdate = true;
            }

            function addColorAttribute(geometry, stops) {
                // const pos = geometry.attributes.position.array;
                const pos = geometry.attributes.planePosition.array;
                const count = pos.length / 3;

                // 找到 x 坐标范围
                let minX = Infinity,
                    maxX = -Infinity;
                for (let i = 0; i < count; i++) {
                    const x = pos[i * 3];
                    minX = Math.min(minX, x);
                    maxX = Math.max(maxX, x);
                }

                const colors = new Float32Array(count * 3);

                for (let i = 0; i < count; i++) {
                    const x = pos[i * 3];
                    const t = (x - minX) / (maxX - minX); // [0,1]

                    // 找到 t 落在哪个区间
                    let r, g, b;
                    for (let j = 0; j < stops.length - 1; j++) {
                        const s1 = stops[j];
                        const s2 = stops[j + 1];
                        if (t >= s1.pos && t <= s2.pos) {
                            // 归一化到 [0,1]
                            const localT = (t - s1.pos) / (s2.pos - s1.pos);
                            r = THREE.MathUtils.lerp(s1.color[0], s2.color[0], localT);
                            g = THREE.MathUtils.lerp(s1.color[1], s2.color[1], localT);
                            b = THREE.MathUtils.lerp(s1.color[2], s2.color[2], localT);
                            break;
                        }
                    }
                    colors[i * 3 + 0] = r;
                    colors[i * 3 + 1] = g;
                    colors[i * 3 + 2] = b;
                }

                geometry.setAttribute("vColor", new THREE.BufferAttribute(colors, 3));
            }

            function shuffleArray(array) {
                for (let i = array.length - 1; i > 0; i--) {
                    const j = Math.floor(Math.random() * (i + 1));
                    [array[i], array[j]] = [array[j], array[i]]; // 交换
                }
                return array;
            }

            function randomUniqueSampling(vertices, sampleCount, cols) {
                const count = vertices.length / 3;
                const indices = Array.from({ length: count }, (_, i) => i);

                // 打乱顺序
                const shuffled = shuffleArray(indices);

                // 采样结果
                const positions = new Float32Array(sampleCount * 3);
                const colors = new Float32Array(sampleCount * 3);
                const overs = new Float32Array(sampleCount * 1);

                for (let i = 0; i < sampleCount; i++) {
                    const idx = shuffled[i % count]; // 如果 sampleCount > 顶点数，可以循环使用

                    // if (i >= count) {

                    overs[i] = i >= count ? 1 : 0;
                    //     positions[i * 3 + 0] = vertices[idx * 3 + 0] + 1000;
                    //     positions[i * 3 + 1] = vertices[idx * 3 + 1];
                    //     positions[i * 3 + 2] = vertices[idx * 3 + 2] + 100;
                    // } else {
                    positions[i * 3 + 0] = vertices[idx * 3 + 0];
                    positions[i * 3 + 1] = vertices[idx * 3 + 1];
                    positions[i * 3 + 2] = vertices[idx * 3 + 2];
                    // }
                    colors[i * 3 + 0] = cols[idx * 3 + 0];
                    colors[i * 3 + 1] = cols[idx * 3 + 1];
                    colors[i * 3 + 2] = cols[idx * 3 + 2];
                }

                positions.colors = colors;
                positions.overs = overs;

                return positions;
            }

            function whiteText(reverse = false,color) {
                return new THREE.ShaderMaterial({
                    uniforms: {
                        // u_opacity: { value: 1 },
                        iProgress: uniforms.iProgress,
                        uTime: uniforms.uTime,
                        speed: { value: reverse ? 2 : 2 },
                        color: { value:color || new THREE.Color(0xffffff) },
                    },
                    vertexShader: `
                        uniform float iProgress;
                        uniform float uTime;

                        void main() {
                            vec3 transformed = position;
                            float opacity = smoothstep( 0.0 , 1. , 2.0 * ( iProgress - 0.5 ) );

                            transformed.y += .2 * sin( 10.0 * transformed.x ) * ( opacity ) * sin( uTime );

                            // uTime 动画
                            ${reverse ? 'transformed.y -=  iProgress * sin(6. * transformed.y) / 20. + iProgress * 2.;' : 'transformed.y += sin( transformed.x / 2. + uTime ) * .5;'}

                            gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
                        }
                    `,
                    fragmentShader: `
                        uniform float iProgress;
                        uniform float speed;
                        uniform vec3 color;

                        void main() {
                            float opacity = smoothstep( 0.0 , 1. , speed * ( iProgress - ( 0.5 ${reverse ? '- 0.5': '' } )  ) );

                            ${reverse ? "opacity = 1.0 - opacity;" : ""}

                            gl_FragColor = vec4(color, opacity);
                        }
                    `,
                    transparent: true,
                });
            }

            // 监听页面滚动
            window.addEventListener("scroll", () => {
                const scrollTop = window.scrollY;
                const height = window.innerHeight;

                const progress = scrollTop / height;
                uniforms.iProgress.value = Math.min(progress, 1);

                console.log(uniforms.iProgress.value);

                // 如果进度大于1 则取消canvas fixed
                if (uniforms.iProgress.value >= 1) {
                    canvas.style.position = "relative";
                } else {
                    canvas.style.position = "fixed";
                }
            });
        </script>
    </body>
</html>
